package uk.ac.cam.tcs40.sidnet;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.TextNode;

import uk.ac.cam.tcs40.sidnet.formal.Attendee;
import uk.ac.cam.tcs40.sidnet.formal.Dietary;
import uk.ac.cam.tcs40.sidnet.formal.Formal;
import uk.ac.cam.tcs40.sidnet.formal.FormalMenu;
import uk.ac.cam.tcs40.sidnet.formal.Guest;
import uk.ac.cam.tcs40.sidnet.menu.Meal;
import uk.ac.cam.tcs40.sidnet.menu.Menu;

public class Hall {

	private static Hall s_Instance;

	private final DateFormat s_DateFormat = new SimpleDateFormat("d MMM yyyy", Locale.getDefault());

	private final String s_RavenLogin = "https://raven.cam.ac.uk/auth/authenticate2.html";
	private final String s_Sidnet = "https://www.sid.cam.ac.uk/sidnet/student/raven-auth/";
	private final String s_Hall = "hall/";


	private final Menu m_Menu = new Menu();
	private final List<Formal> m_FormalList = new LinkedList<Formal>();

	private boolean m_MenuDownloaded;
	private boolean m_FormalsDownloaded;

	private final HttpClient m_Client;
	private final CookieStore m_CookieStore = new BasicCookieStore();
	private final HttpContext m_HttpContext = new BasicHttpContext();

	private Hall() {
		HttpParams params = new BasicHttpParams();
		HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
		HttpProtocolParams.setContentCharset(params, "utf-8");
		params.setBooleanParameter("http.protocol.expect-continue", false);

		SchemeRegistry registry = new SchemeRegistry();
		registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
		final SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory();
		sslSocketFactory.setHostnameVerifier(SSLSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
		registry.register(new Scheme("https", sslSocketFactory, 443));

		ThreadSafeClientConnManager manager = new ThreadSafeClientConnManager(params, registry);
		m_Client = new DefaultHttpClient(manager, params);

		m_Client.getParams().setParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true); 

		m_HttpContext.setAttribute(ClientContext.COOKIE_STORE, m_CookieStore);

		s_Instance = this;
	}

	public static Hall getInstance() {
		if (s_Instance == null)
			s_Instance = new Hall();
		return s_Instance;
	}

	public boolean haveDownloadedMenu() {
		return m_MenuDownloaded;
	}

	public boolean haveDownloadedFormals() {
		return m_FormalsDownloaded;
	}

	public List<Formal> getFormals() {
		synchronized (m_FormalList) {
			return m_FormalList;
		}
	}

	public int getFormalCount() {
		synchronized (m_FormalList) {
			return m_FormalList.size();
		}
	}

	public Formal getFormal(String id) {
		synchronized (m_FormalList) {
			for (Formal formal : m_FormalList) {
				if (formal.getID().equals(id))
					return formal;
			}
			return null;
		}
	}

	public Menu getMenu() {
		synchronized (m_Menu) {
			return m_Menu;
		}
	}

	public boolean login(String username, String password, StringBuilder errorString) {

		final HttpPost post = new HttpPost(s_RavenLogin);

		try {
			final List<NameValuePair> nameValuePairs = 	new ArrayList<NameValuePair>(3);
			nameValuePairs.add(new BasicNameValuePair("userid", username));
			nameValuePairs.add(new BasicNameValuePair("pwd", password));
			nameValuePairs.add(new BasicNameValuePair("submit", "Submit"));

			post.setEntity(new UrlEncodedFormEntity(nameValuePairs));

			final HttpResponse login = m_Client.execute(post, m_HttpContext);

			Document document = Jsoup.parse(readResponse(login));

			Element content = document.getElementById("content");

			Element error = content.getElementsByClass("error").first();

			if (error == null) 
				return true;

			errorString.append(error.ownText());
			return false;

		} catch (final IOException e) {
			e.printStackTrace();
			return false;
		}
	}

	public void downloadMenu() {
		synchronized (m_Menu) {
			try {

				final HttpGet get = new HttpGet(s_Sidnet + "index.html");
				final HttpResponse sidnet = m_Client.execute(get, m_HttpContext);

				Document document = Jsoup.parse(readResponse(sidnet));

				Element menu = document.getElementById("rightcol");

				for (Element img : menu.getElementsByTag("img"))
					img.replaceWith(new TextNode(" (*)", ""));

				boolean today = false, lunch = false;
				Meal meal;

				for (Element e : menu.getAllElements()) {

					if (e.tagName().equals("p")) {
						if (!e.text().contains("indicates")) {
							if (e.text().startsWith("v. "))
								meal = new Meal(e.text().substring(3), today, lunch, true);
							else
								meal = new Meal(e.text(), today, lunch, false);

							m_Menu.addMeal(meal);
						}

					} else if (e.tagName().equals("span")) {
						today = e.text().equals("Today");

					} else if (e.tagName().equals("h3")) {
						lunch = e.text().equals("Lunch") || e.text().equals("Brunch");
					}
				}
				m_MenuDownloaded = true;

			} catch (final IOException e) {
				e.printStackTrace();
			} catch (final ArrayIndexOutOfBoundsException e) {
				e.printStackTrace();
			}
		}
	}

	public synchronized int downloadFormalList() {
		try {
			final HttpGet get = new HttpGet(s_Sidnet + s_Hall + "index.php");
			final HttpResponse sidnet = m_Client.execute(get, m_HttpContext);

			Document document = Jsoup.parse(readResponse(sidnet));

			Element formalElement;

			String id = null;
			Date date;
			boolean attending;

			synchronized (m_FormalList) {
				m_FormalList.clear();
			}

			for (Element e : document.getElementsByClass("menutip")) {
				// id="mt314_t"
				id = e.id().substring(2, 5);

				formalElement = document.getElementById(e.id().replace("_t", "")).parent();

				date = s_DateFormat.parse(formalElement.getElementById(e.id().replace("_t", "")).text().replaceAll("st|nd|rd|th", ""));

				if (formalElement.text().contains("not signed up"))
					attending = false;
				else
					attending = true;
							
				String starter = e.textNodes().get(0).text();
				String main = e.textNodes().get(1).text();
				String dessert = e.textNodes().get(2).text();
				String vegetarian = e.getElementsByTag("em").text();
				vegetarian = vegetarian.substring(4, vegetarian.length() - 1);
				
				FormalMenu menu = new FormalMenu(starter, main, vegetarian, dessert);
				
				Formal formal = new Formal(id, date, attending, menu);

				synchronized (m_FormalList) {
					m_FormalList.add(formal);
				}
			}
			m_FormalsDownloaded = true;

			if (id != null)
				return Integer.valueOf(id);
			else
				return -1;

		} catch (final IOException e) {
			e.printStackTrace();
			return -1;
		} catch (final ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
			return -1;
		} catch (ParseException e) {
			e.printStackTrace();
			return -1;
		}
	}

	public boolean findFormals(int id) {

		if (id > 0) {

			Formal formal;

			if ((formal = getFormalAttendingDetails(id)) != null) {
				synchronized (m_FormalList) {
					m_FormalList.add(formal);
				}
				return true;
			}
		}
		return false;
	}

	public Formal getFormalAttendingDetails(int id) {
		try {			
			final HttpGet get = new HttpGet(s_Sidnet + s_Hall + "attending.php?mid=" + id);
			final HttpResponse sidnet = m_Client.execute(get, m_HttpContext);

			if (sidnet.getStatusLine().getStatusCode() != HttpStatus.SC_OK)
				return null;

			Document document = Jsoup.parse(readResponse(sidnet));

			Element attending = document.getElementById("content");

			// create new formal				
			Date date = s_DateFormat.parse(attending.getElementsByTag("h3").first().text().replaceAll("st|nd|rd|th", "").replace("Formal Hall - ", ""));

			Formal formal = new Formal(String.valueOf(id), date, false, null);
					
			for (Element el : attending.getElementsByTag("span"))
				if (el.className().equals("halltab0s"))
					formal.addToAttendingList(new Attendee(el.text(), false));
				else if (el.className().equals("halltab0g"))
					formal.addToAttendingList(new Attendee(el.text().substring(0, el.text().lastIndexOf(",")), true));
			
			return formal;

		} catch (final IOException e) {
			e.printStackTrace();
			return null;
		} catch (final ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
			return null;
		} catch (ParseException e) {
			e.printStackTrace();
			return null;
		}
	}

	public void getFormalAttendingDetails(Formal formal) {
		try {
			final HttpGet get = new HttpGet(s_Sidnet + s_Hall + "attending.php?mid=" + formal.getID());
			final HttpResponse sidnet = m_Client.execute(get, m_HttpContext);

			Document document = Jsoup.parse(readResponse(sidnet));

			Element attending = document.getElementById("content");

			formal.clearAttendingList();
			
			for (Element el : attending.getElementsByTag("span"))
				if (el.className().equals("halltab0s"))
					formal.addToAttendingList(new Attendee(el.text(), false));
				else if (el.className().equals("halltab0g"))
					formal.addToAttendingList(new Attendee(el.text().substring(0, el.text().lastIndexOf(",")), true));

		} catch (final IOException e) {
			e.printStackTrace();
		} catch (final ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
		}
	}

	public boolean bookFormal(Formal formal) {
		return bookFormal(formal, formal.getUser().isAttending());
	}

	private boolean bookFormal(Formal formal, boolean attending) {

		final HttpPost post = new HttpPost(s_Sidnet + s_Hall + "confirm.php");

		try {
			final List<NameValuePair> nameValuePairs = 	new ArrayList<NameValuePair>(3);
			nameValuePairs.add(new BasicNameValuePair("mid", formal.getID()));

			synchronized (formal) {

				if (!attending) {
					nameValuePairs.add(new BasicNameValuePair("cancel", "on"));
				} else {

					nameValuePairs.add(new BasicNameValuePair("s0[sid]", "0"));
					nameValuePairs.add(new BasicNameValuePair("s0[dietary]", formal.getUser().getDietary().getCode()));
					nameValuePairs.add(new BasicNameValuePair("s0[notes]", formal.getUser().getNotes()));

					int guestNumber;
					int guestCount = formal.guestCount();
					Guest guest;

					for (guestNumber = 0; guestNumber < guestCount; guestNumber++) {
						// This guest ID actually needs to be set.
						guest = formal.getGuests().get(guestNumber);
						nameValuePairs.add(new BasicNameValuePair("g" + guestNumber + "[sid]", "0"));
						nameValuePairs.add(new BasicNameValuePair("g" + guestNumber + "[name]", guest.getName()));
						nameValuePairs.add(new BasicNameValuePair("g" + guestNumber + "[dietary]", guest.getDietary().getCode()));
						nameValuePairs.add(new BasicNameValuePair("g" + guestNumber + "[notes]", guest.getNotes()));
					}
					for (; guestNumber < formal.getSidnetGuestIDList().size(); guestNumber++) {
						nameValuePairs.add(new BasicNameValuePair("g" + guestNumber + "[sid]", formal.getSidnetGuestIDList().get(guestNumber)));
						nameValuePairs.add(new BasicNameValuePair("g" + guestNumber + "[name]", " "));
						nameValuePairs.add(new BasicNameValuePair("g" + guestNumber + "[cancel]", "on"));
					}
				}

				nameValuePairs.add(new BasicNameValuePair("book", "Make Booking"));

				post.setEntity(new UrlEncodedFormEntity(nameValuePairs));
				final HttpResponse book = m_Client.execute(post, m_HttpContext);

				String response = readResponse(book);
			}

			//Document document = Jsoup.parse(builder.toString());
			return true;

		} catch (final IOException e) {
			e.printStackTrace();
			return false;
		}
	}

	public void getFormalBookingDetails(Formal formal) {
		try {			
			final HttpGet get = new HttpGet(s_Sidnet + s_Hall + "booking.html?mid=" + formal.getID());
			final HttpResponse sidnet = m_Client.execute(get, m_HttpContext);

			Document document = Jsoup.parse(readResponse(sidnet));

			Element form = document.getElementsByClass("maintinfo").first();

			String dietaryCode = form.select("select[name^=s0][name*=dietary] > option[selected=selected]").first().val();
			formal.getUser().setDietary(Dietary.fromString(dietaryCode));

			String notes = form.select("textarea[name^=s0][name*=notes]").first().val();
			formal.getUser().setNotes(notes);

			formal.clearGuests();
			formal.clearSidnetGuestIDList();

			setGuestDetails(formal, form);

		} catch (final IOException e) {
			e.printStackTrace();
		} catch (final ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
		}
	}

	private void setGuestDetails(Formal formal, Element form) {

		int guest = 0;
		while (true) {

			String guestSidnet = form.select("input[name^=g" + guest + "][name*=sid]").first().val();

			if (guestSidnet.equals("0"))
				break;

			formal.addSidnetGuestID(guestSidnet);

			String name = form.select("input[name^=g" + guest + "][name*=name]").first().val();
			String dietaryCode = form.select("select[name^=g" + guest + "][name*=dietary] > option[selected=selected]").first().val();
			String notes = form.select("textarea[name^=g" + guest + "][name*=notes]").first().val();

			formal.addGuest(new Guest(name, Dietary.fromString(dietaryCode), notes));

			guest++;
		}
	}
	
	public void getFormalSidnetIDList(Formal formal) {
		try {			
			final HttpGet get = new HttpGet(s_Sidnet + s_Hall + "booking.html?mid=" + formal.getID());
			final HttpResponse sidnet = m_Client.execute(get, m_HttpContext);

			Document document = Jsoup.parse(readResponse(sidnet));

			Element form = document.getElementsByClass("maintinfo").first();

			formal.clearSidnetGuestIDList();
			
			int guest = 0;
			while (true) {

				String guestSidnet = form.select("input[name^=g" + guest + "][name*=sid]").first().val();

				if (guestSidnet.equals("0"))
					break;

				formal.addSidnetGuestID(guestSidnet);

				guest++;
			}

		} catch (final IOException e) {
			e.printStackTrace();
		} catch (final ArrayIndexOutOfBoundsException e) {
			e.printStackTrace();
		}
	}

	private String readResponse(HttpResponse response) throws IllegalStateException, IOException {
		final BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));

		String line = null;
		StringBuilder builder = new StringBuilder();

		while ((line = reader.readLine()) != null) {
			builder.append(line);
		}

		return builder.toString();

	}
}
